#include <GL/glew.h>
#include <GL/freeglut.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <cstdio>
#include <ctime>
#include <common/common.hpp>
#include "mallet.hpp"
#include "table.hpp"
#include "puck.hpp"
#include "color_shader.hpp"
#include "texture_shader.hpp"

color_shader _colorshader;
texture_shader _texshader;
GLuint _texid;

mallet _mallet(0.08f, 0.15f, 32);
table _table;
puck _puck(0.06f, 0.02, 32);

bool _is_mallet_pressed;
point _blue_mallet_pos(0, 0, 0);
point _prev_blue_mallet_pos(0, 0, 0);

point _puck_pos(0, 0, 0);
vector _puck_dir(0, 0, 0); // meas direction and the length meas speed

glm::mat4 _view_projection_mat;
glm::mat4 _inverse_view_projection_mat;

// 要想知道在窗口上点击时，是否点击到三维空间中的某个物体，需要先把窗口上点击时的二维坐标转换到三维空间，
// 具体会被转换成三维空间中的一条直线，然后判断该直线是否与物体相交。下面就是具体的实现。
// 把一个 2d ndc 转换成世界坐标空间中的一条射线。
static ray
normalized2d_to_ray(float nx, float ny) {
	// 世界坐标 - 视图矩阵 - 透视矩阵 - 透视除法 - ndc
	// 要得到反转得到世界坐标，先需要视图矩阵和透视矩阵的反转矩阵

	// ndc 坐标系是左手坐标系，所以近平面的 z 坐标为远平面的 z 坐标要小
	glm::vec4 nearpoint_ndc(nx, ny, -1, 1);
	glm::vec4 farpoint_ndc(nx, ny, 1, 1);

	glm::vec4 nearpoint_world = _inverse_view_projection_mat * nearpoint_ndc;
	glm::vec4 farpoint_world = _inverse_view_projection_mat * farpoint_ndc;

	// 消除矩阵反转后，透视除法的影响
	nearpoint_world /= nearpoint_world.w;
	farpoint_world /= farpoint_world.w;

	point n(nearpoint_world.x, nearpoint_world.y, nearpoint_world.z);
	point f(farpoint_world.x, farpoint_world.y, farpoint_world.z);

	return ray(n, geometry::vectorbetween(n, f));
}

static GLboolean
init() {
	GLenum err = glewInit();
	if (err != GLEW_OK) {
		logv("fail to init glew");
		return GL_FALSE;
	}

	if (!GLEW_VERSION_3_3) {
		logv("not support for ogl3.3");
		return GL_FALSE;
	}
	logv("support for ogl3.3");

	glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

	_texid = texture_util::loadtexture("./res/air_hockey_surface.png");
	if (_texid == 0) {
		logv("fail to load texture file");
		return GL_FALSE;
	}

    if (!_colorshader.init("./res/simple_vertex_shader.glsl", "./res/simple_fragment_shader.glsl")) {
    	logv("fail to init color shader");
    	return GL_FALSE;
    }

    if (!_texshader.init("./res/texture_vertex_shader.glsl", "./res/texture_fragment_shader.glsl")) {
    	logv("fail to init texture shader");
    	return GL_FALSE;
    }

    
    _blue_mallet_pos = point(0.0f, _mallet.getheight()/2, 0.4f);
    _puck_pos = point(0.0f, _puck.getheight()/2, 0.0f);

    logv("init finish successfully");
    return GL_TRUE;
}

static void
position_table_inscene(glm::mat4 &modelmat) {
	modelmat = glm::mat4();
	modelmat = glm::rotate(modelmat, glm::radians(-90.0f), glm::vec3(1.0f, 0.0f, 0.0f));
	modelmat = _view_projection_mat * modelmat;
}

static void
position_object_inscene(GLfloat x, GLfloat y, GLfloat z, glm::mat4 &modelmat) {
	modelmat = glm::mat4();
	modelmat = glm::translate(modelmat, glm::vec3(x, y, z));
	modelmat = _view_projection_mat * modelmat;
}

static void
cb_display() {
	// logv("cb_display");

   	glClear(GL_COLOR_BUFFER_BIT);

   	glm::mat4 modelmat;
   	position_table_inscene(modelmat);
   	_texshader.useprogram();
   	_texshader.setuniform(modelmat, _texid);
   	_table.binddata();
   	_table.draw();

   	position_object_inscene(0.0f, _mallet.getheight() / 2.0f, -0.4f, modelmat);
   	_colorshader.useprogram();
   	_colorshader.setuniform(modelmat, 1.0f, 0.0f, 0.0f);
   	_mallet.binddata();
   	_mallet.draw();

	// position_object_inscene(0.0f, _mallet.getheight() / 2.0f, 0.4f, modelmat);
	position_object_inscene(_blue_mallet_pos.x, _blue_mallet_pos.y, _blue_mallet_pos.z, modelmat);
	_colorshader.setuniform(modelmat, 0.0f, 0.0f, 1.0f);
   	_mallet.draw();   	

   	// _puck_pos.print();
   	// _puck_dir.print();
   	_puck_pos = _puck_pos.translate(_puck_dir);
   	_table.restrict_puck(_puck_pos, _puck_dir, _puck.getradius());
   	_puck_dir = _puck_dir.scale(0.99f); // add friction
   	position_object_inscene(_puck_pos.x, _puck_pos.y, _puck_pos.z, modelmat);
   	_colorshader.setuniform(modelmat, 0.8f, 0.8f, 1.0f);
   	_puck.binddata();
   	_puck.draw();

	glFlush();
}

static void 
cb_reshape(int width, int height) {
	logv("cb_reshape: width:%d, height:%d", width, height);
	glViewport (0, 0, width, height);

	GLfloat aspect = (GLfloat)width / (GLfloat)height;
	glm::mat4x4 persmat = glm::perspective(glm::radians(45.0f), aspect, 1.0f, 10.0f);
	glm::mat4x4 viewmat = glm::lookAt(glm::vec3(0.0f, 1.2f, 2.2f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
	_view_projection_mat = persmat * viewmat;
	_inverse_view_projection_mat = glm::inverse(_view_projection_mat);
}

static void
cb_close() {
	logv("cb_close");
	_colorshader.deleteprogram();
	_texshader.deleteprogram();
}

static void 
handlepress(float nx, float ny) {
	logv("press, nx:%.2f ny:%.2f", nx, ny);
	ray r = normalized2d_to_ray(nx, ny);
	sphere bounding_sphere(_blue_mallet_pos, _mallet.getheight());
	// r.print();
	// _blue_mallet_pos.print();

	_is_mallet_pressed = geometry::intersect(bounding_sphere, r);
	logv("is hit mallet:%d", _is_mallet_pressed);
}

static void 
cb_mouse(int button, int state, int x, int y) {
	logv("cb_mouse, button:%d state:%d x:%d y:%d", button, state, x, y);
	int width = glutGet(GLUT_WINDOW_WIDTH);
	int height = glutGet(GLUT_WINDOW_HEIGHT);
	float nx, ny;
	turn_screencoord_to_ndc(width, height, x, y, &nx, &ny);

	if (state == GLUT_DOWN)
		handlepress(nx, ny);
}

static void 
handledrag(float nx, float ny) {
	// logv("drag, nx:%f ny:%f", nx, ny);
	if (!_is_mallet_pressed)
		return;

	static clock_t prevclock = 0;
	clock_t nowclock = clock();
	if (nowclock - prevclock < 100)
		return;
	prevclock = nowclock;

	_prev_blue_mallet_pos = _blue_mallet_pos;

	ray r = normalized2d_to_ray(nx, ny);
	plane p = plane(point(0.0f, 0.0f, 0.0f), vector(0.0f, 1.0f, 0.0f)); // the plane representing the air hockey table
	point touchpoint = geometry::intersectionpoint(r, p);
	_blue_mallet_pos = point(touchpoint.x, _mallet.getheight(), touchpoint.z);
	float radius = _mallet.getheight() / 2;
    _blue_mallet_pos = _table.restrict_blue_mallet(_blue_mallet_pos.x, _blue_mallet_pos.y, _blue_mallet_pos.z, radius);

    float distance = geometry::vectorbetween(_blue_mallet_pos, _puck_pos).length();
    float tolerance = 0.1f;
    // logv("hit test, distance:%.2f, total:%.2f", distance, _puck.getradius() + _mallet.getradius());
    if (distance < (_puck.getradius() + _mallet.getradius() + tolerance)) {
    	_puck_dir = geometry::vectorbetween(_prev_blue_mallet_pos, _blue_mallet_pos);
    	logv("hit the puck, dir<%.2f,%.2f,%.2f>", _puck_dir.x, _puck_dir.y, _puck_dir.z);
    }

    glutPostRedisplay();
}

static void 
cb_motion(int x, int y) {
	// logv("motion, x:%d y:%d", x, y);
	int width = glutGet(GLUT_WINDOW_WIDTH);
	int height = glutGet(GLUT_WINDOW_HEIGHT);
	float nx, ny;
	turn_screencoord_to_ndc(width, height, x, y, &nx, &ny);

	handledrag(nx, ny);
}

static void
cb_timer(int param) {
	glutPostRedisplay();
	glutTimerFunc(500, cb_timer, 0);
}

int
main(int argc, char *argv[]) {
	glutInit(&argc, argv);
	glutInitWindowSize(800, 600);
	glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
	glutInitWindowPosition(0, 0);
	
	glutCreateWindow("AirHockey - touch");
	
	if (init() == GL_FALSE)
		return 0;

	glutDisplayFunc(cb_display);
	glutReshapeFunc(cb_reshape);
	glutCloseFunc(cb_close);
	glutMouseFunc(cb_mouse);
	glutMotionFunc(cb_motion);
	glutTimerFunc(500, cb_timer, 0);

	glutMainLoop();
	return 0;
}
